import math
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

from models.m import (
    AuditStatus,
    Bookshelf,
    Novel,
    NovelCategory,
    NovelStatus,
    User,
    UserTagRelation,
)

tueijian_router = APIRouter(prefix="/tueijian", tags=["智能推荐"])


class RecommendedNovel(BaseModel):
    id: int
    title: str
    author: str
    cover_url: Optional[str]
    description: Optional[str]
    category_id: int
    category_name: Optional[str]
    rating: float
    rating_count: int
    view_count: int
    collect_count: int
    tags: List[str]
    score: float


class RecommendationResult(BaseModel):
    user_id: Optional[int]
    strategy: str
    items: List[RecommendedNovel]


async def _fetch_user_preferences(user_id: int) -> Tuple[Counter, Counter, set[int]]:
    """
    根据用户书架推断偏好：
    - 统计最常阅读的分类
    - 统计出现频率最高的标签
    - 记录已经阅读过的小说，避免重复推荐
    """
    category_counter: Counter = Counter()
    tag_counter: Counter = Counter()
    owned_novel_ids: set[int] = set()

    bookshelf_items = await Bookshelf.filter(user_id=user_id).prefetch_related("novel", "novel__category")

    for entry in bookshelf_items:
        if not entry.novel:
            continue
        owned_novel_ids.add(entry.novel_id)
        if entry.novel.category_id:
            category_counter[entry.novel.category_id] += 1

        tags = entry.novel.tags or []
        if isinstance(tags, list):
            tag_counter.update(tag for tag in tags if isinstance(tag, str))
        elif isinstance(tags, dict):
            tag_counter.update(str(v) for v in tags.values())
        elif isinstance(tags, str):
            tag_counter.update(tag.strip() for tag in tags.split(",") if tag.strip())

    if not category_counter and not tag_counter:
        # 用户书架没有偏好时，再尝试读取用户标签画像
        relations = await UserTagRelation.filter(user_id=user_id).prefetch_related("tag")
        for rel in relations:
            if rel.tag:
                tag_counter[rel.tag.name] += 1

    return category_counter, tag_counter, owned_novel_ids


def _compute_base_score(novel: Novel) -> float:
    rating = float(novel.rating or 0)
    view_score = math.log1p(novel.view_count or 0)
    collect_score = math.log1p(novel.collect_count or 0)
    featured_bonus = 1.5 if getattr(novel, "is_featured", False) else 0.0
    status_bonus = 0.2 if novel.status == NovelStatus.COMPLETED else 0.0
    return rating * 1.5 + view_score * 1.2 + collect_score + featured_bonus + status_bonus


def _apply_personalization(
    novel: Novel,
    base_score: float,
    category_counter: Counter,
    tag_counter: Counter,
    category_map: Dict[int, NovelCategory],
) -> float:
    if not category_counter and not tag_counter:
        return base_score

    personalized_score = base_score

    if category_counter and novel.category_id in category_counter:
        max_cat_frequency = max(category_counter.values())
        cat_weight = category_counter[novel.category_id] / max(1, max_cat_frequency)
        personalized_score += 2.0 * cat_weight

    raw_tags = novel.tags or []
    novel_tags: List[str]
    if isinstance(raw_tags, list):
        novel_tags = [tag for tag in raw_tags if isinstance(tag, str)]
    elif isinstance(raw_tags, dict):
        novel_tags = [str(v) for v in raw_tags.values()]
    elif isinstance(raw_tags, str):
        novel_tags = [tag.strip() for tag in raw_tags.split(",") if tag.strip()]
    else:
        novel_tags = []

    if tag_counter and novel_tags:
        max_tag_frequency = max(tag_counter.values())
        overlap_score = sum(tag_counter[tag] for tag in novel_tags if tag in tag_counter)
        if overlap_score:
            personalized_score += 1.5 * (overlap_score / max(1, max_tag_frequency))

    # 若用户偏好中缺少当前小说分类，但分类热度整体很高，也适当给权重，防止热门题材被完全过滤
    if category_counter and novel.category_id not in category_counter:
        personalized_score += 0.3 * (len(category_counter) / (len(category_map) or 1))

    return personalized_score


async def _collect_candidates(limit: int, multiplier: int = 4) -> List[Novel]:
    candidate_size = max(limit * multiplier, 60)
    return (
        await Novel.filter(audit_status=AuditStatus.APPROVED)
        .prefetch_related("category")
        .order_by("-is_featured", "-rating", "-view_count", "-collect_count", "-updated_at")
        .limit(candidate_size)
    )


async def _build_recommendations(
    candidates: List[Novel],
    limit: int,
    category_counter: Counter,
    tag_counter: Counter,
    owned_novel_ids: set[int],
) -> List[RecommendedNovel]:
    category_ids = {novel.category_id for novel in candidates if novel.category_id}
    category_map_raw = await NovelCategory.filter(id__in=list(category_ids))
    category_map: Dict[int, NovelCategory] = {category.id: category for category in category_map_raw}

    scored_items: List[Tuple[float, Novel]] = []

    for novel in candidates:
        if novel.id in owned_novel_ids:
            continue

        base_score = _compute_base_score(novel)
        final_score = _apply_personalization(novel, base_score, category_counter, tag_counter, category_map)

        scored_items.append((final_score, novel))

    scored_items.sort(key=lambda item: item[0], reverse=True)

    recommendations: List[RecommendedNovel] = []
    for score, novel in scored_items[:limit]:
        category_name = None
        if novel.category_id and novel.category_id in category_map:
            category_name = category_map[novel.category_id].name

        raw_tags = novel.tags or []
        if isinstance(raw_tags, list):
            tags = [tag for tag in raw_tags if isinstance(tag, str)]
        elif isinstance(raw_tags, dict):
            tags = [str(v) for v in raw_tags.values()]
        elif isinstance(raw_tags, str):
            tags = [tag.strip() for tag in raw_tags.split(",") if tag.strip()]
        else:
            tags = []

        recommendations.append(
            RecommendedNovel(
                id=int(novel.id),
                title=str(novel.title),
                author=str(novel.author),
                cover_url=getattr(novel, "cover_url", None),
                description=getattr(novel, "description", None),
                category_id=int(novel.category_id),
                category_name=category_name,
                rating=float(novel.rating or 0),
                rating_count=int(novel.rating_count or 0),
                view_count=int(novel.view_count or 0),
                collect_count=int(novel.collect_count or 0),
                tags=tags,
                score=round(score, 4),
            )
        )

    return recommendations


@tueijian_router.get("/novels", response_model=RecommendationResult, summary="小说智能推荐")
async def recommend_novels(
    user_id: int | None = Query(default=None, description="用户ID，可选"),
    limit: int = Query(default=10, ge=1, le=50, description="返回推荐数量"),
) -> RecommendationResult:
    """
    综合使用热门度 + 用户偏好（书架、标签）来生成推荐列表。
    如果未传入用户ID，则退化为热门榜单。
    """
    try:
        if user_id is not None:
            user_exists = await User.exists(id=user_id)
            if not user_exists:
                raise HTTPException(status_code=404, detail=f"用户 {user_id} 不存在")

        candidates = await _collect_candidates(limit)

        if not candidates:
            return RecommendationResult(user_id=user_id, strategy="empty", items=[])

        if user_id is None:
            category_counter = Counter()
            tag_counter = Counter()
            owned_ids: set[int] = set()
            strategy = "hot"
        else:
            category_counter, tag_counter, owned_ids = await _fetch_user_preferences(user_id)
            strategy = "personalized" if (category_counter or tag_counter) else "hot_fallback"

        recommendations = await _build_recommendations(candidates, limit, category_counter, tag_counter, owned_ids)

        return RecommendationResult(user_id=user_id, strategy=strategy, items=recommendations)

    except HTTPException:
        raise
    except Exception as exc:
        raise HTTPException(status_code=500, detail=f"智能推荐失败: {exc}") from exc

